package org.apache.maven.tools.plugin.generator; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.text.MutableAttributeSet; import javax.swing.text.html.HTML; import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.parser.ParserDelegator; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.project.MavenProject; import org.apache.maven.reporting.MavenReport; import org.codehaus.plexus.component.repository.ComponentDependency; import org.codehaus.plexus.util.StringUtils; import org.codehaus.plexus.util.xml.XMLWriter; import org.w3c.tidy.Tidy; /** * Convenience methods to play with Maven plugins. * * @author jdcasey * @version $Id$ */ public final class GeneratorUtils { private GeneratorUtils() { // nop } /** * @param w not null writer * @param pluginDescriptor not null */ public static void writeDependencies( XMLWriter w, PluginDescriptor pluginDescriptor ) { w.startElement( "dependencies" ); @SuppressWarnings( "unchecked" ) List<ComponentDependency> deps = pluginDescriptor.getDependencies(); for ( ComponentDependency dep : deps ) { w.startElement( "dependency" ); element( w, "groupId", dep.getGroupId() ); element( w, "artifactId", dep.getArtifactId() ); element( w, "type", dep.getType() ); element( w, "version", dep.getVersion() ); w.endElement(); } w.endElement(); } /** * @param w not null writer * @param name not null * @param value could be null */ public static void element( XMLWriter w, String name, String value ) { w.startElement( name ); if ( value == null ) { value = ""; } w.writeText( value ); w.endElement(); } public static void element( XMLWriter w, String name, String value, boolean asText ) { element( w, name, asText ? GeneratorUtils.toText( value ) : value ); } /** * @param dependencies not null list of <code>Dependency</code> * @return list of component dependencies */ public static List<ComponentDependency> toComponentDependencies( List<Dependency> dependencies ) { List<ComponentDependency> componentDeps = new LinkedList<ComponentDependency>(); for ( Dependency dependency : dependencies ) { ComponentDependency cd = new ComponentDependency(); cd.setArtifactId( dependency.getArtifactId() ); cd.setGroupId( dependency.getGroupId() ); cd.setVersion( dependency.getVersion() ); cd.setType( dependency.getType() ); componentDeps.add( cd ); } return componentDeps; } /** * Returns a literal replacement <code>String</code> for the specified <code>String</code>. This method * produces a <code>String</code> that will work as a literal replacement <code>s</code> in the * <code>appendReplacement</code> method of the {@link Matcher} class. The <code>String</code> produced will * match the sequence of characters in <code>s</code> treated as a literal sequence. Slashes ('\') and dollar * signs ('$') will be given no special meaning. TODO: copied from Matcher class of Java 1.5, remove once target * platform can be upgraded * * @see <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/Matcher.html">java.util.regex.Matcher</a> * @param s The string to be literalized * @return A literal string replacement */ private static String quoteReplacement( String s ) { if ( ( s.indexOf( '\\' ) == -1 ) && ( s.indexOf( '$' ) == -1 ) ) { return s; } StringBuilder sb = new StringBuilder(); for ( int i = 0; i < s.length(); i++ ) { char c = s.charAt( i ); if ( c == '\\' ) { sb.append( '\\' ); sb.append( '\\' ); } else if ( c == '$' ) { sb.append( '\\' ); sb.append( '$' ); } else { sb.append( c ); } } return sb.toString(); } /** * Decodes javadoc inline tags into equivalent HTML tags. For instance, the inline tag "{@code <A&B>}" should be * rendered as "<code><A&B></code>". * * @param description The javadoc description to decode, may be <code>null</code>. * @return The decoded description, never <code>null</code>. */ static String decodeJavadocTags( String description ) { if ( StringUtils.isEmpty( description ) ) { return ""; } StringBuffer decoded = new StringBuffer( description.length() + 1024 ); Matcher matcher = Pattern.compile( "\\{@(\\w+)\\s*([^\\}]*)\\}" ).matcher( description ); while ( matcher.find() ) { String tag = matcher.group( 1 ); String text = matcher.group( 2 ); text = StringUtils.replace( text, "&", "&" ); text = StringUtils.replace( text, "<", "<" ); text = StringUtils.replace( text, ">", ">" ); if ( "code".equals( tag ) ) { text = "<code>" + text + "</code>"; } else if ( "link".equals( tag ) || "linkplain".equals( tag ) || "value".equals( tag ) ) { String pattern = "(([^#\\.\\s]+\\.)*([^#\\.\\s]+))?" + "(#([^\\(\\s]*)(\\([^\\)]*\\))?\\s*(\\S.*)?)?"; final int label = 7; final int clazz = 3; final int member = 5; final int args = 6; Matcher link = Pattern.compile( pattern ).matcher( text ); if ( link.matches() ) { text = link.group( label ); if ( StringUtils.isEmpty( text ) ) { text = link.group( clazz ); if ( StringUtils.isEmpty( text ) ) { text = ""; } if ( StringUtils.isNotEmpty( link.group( member ) ) ) { if ( StringUtils.isNotEmpty( text ) ) { text += '.'; } text += link.group( member ); if ( StringUtils.isNotEmpty( link.group( args ) ) ) { text += "()"; } } } } if ( !"linkplain".equals( tag ) ) { text = "<code>" + text + "</code>"; } } matcher.appendReplacement( decoded, ( text != null ) ? quoteReplacement( text ) : "" ); } matcher.appendTail( decoded ); return decoded.toString(); } /** * Fixes some javadoc comment to become a valid XHTML snippet. * * @param description Javadoc description with HTML tags, may be <code>null</code>. * @return The description with valid XHTML tags, never <code>null</code>. */ public static String makeHtmlValid( String description ) { if ( StringUtils.isEmpty( description ) ) { return ""; } String commentCleaned = decodeJavadocTags( description ); // Using jTidy to clean comment Tidy tidy = new Tidy(); tidy.setDocType( "loose" ); tidy.setXHTML( true ); tidy.setXmlOut( true ); tidy.setInputEncoding( "UTF-8" ); tidy.setOutputEncoding( "UTF-8" ); tidy.setMakeClean( true ); tidy.setNumEntities( true ); tidy.setQuoteNbsp( false ); tidy.setQuiet( true ); tidy.setShowWarnings( false ); try { ByteArrayOutputStream out = new ByteArrayOutputStream( commentCleaned.length() + 256 ); tidy.parse( new ByteArrayInputStream( commentCleaned.getBytes( "UTF-8" ) ), out ); commentCleaned = out.toString( "UTF-8" ); } catch ( UnsupportedEncodingException e ) { // cannot happen as every JVM must support UTF-8, see also class javadoc for java.nio.charset.Charset } if ( StringUtils.isEmpty( commentCleaned ) ) { return ""; } // strip the header/body stuff String ls = System.getProperty( "line.separator" ); int startPos = commentCleaned.indexOf( "<body>" + ls ) + 6 + ls.length(); int endPos = commentCleaned.indexOf( ls + "</body>" ); commentCleaned = commentCleaned.substring( startPos, endPos ); return commentCleaned; } /** * Converts a HTML fragment as extracted from a javadoc comment to a plain text string. This method tries to retain * as much of the text formatting as possible by means of the following transformations: * <ul> * <li>List items are converted to leading tabs (U+0009), followed by the item number/bullet, another tab and * finally the item contents. Each tab denotes an increase of indentation.</li> * <li>Flow breaking elements as well as literal line terminators in preformatted text are converted to a newline * (U+000A) to denote a mandatory line break.</li> * <li>Consecutive spaces and line terminators from character data outside of preformatted text will be normalized * to a single space. The resulting space denotes a possible point for line wrapping.</li> * <li>Each space in preformatted text will be converted to a non-breaking space (U+00A0).</li> * </ul> * * @param html The HTML fragment to convert to plain text, may be <code>null</code>. * @return A string with HTML tags converted into pure text, never <code>null</code>. * @since 2.4.3 */ public static String toText( String html ) { if ( StringUtils.isEmpty( html ) ) { return ""; } final StringBuilder sb = new StringBuilder(); HTMLEditorKit.Parser parser = new ParserDelegator(); HTMLEditorKit.ParserCallback htmlCallback = new MojoParserCallback( sb ); try { parser.parse( new StringReader( makeHtmlValid( html ) ), htmlCallback, true ); } catch ( IOException e ) { throw new RuntimeException( e ); } return sb.toString().replace( '\"', '\'' ); // for CDATA } /** * ParserCallback implementation. */ private static class MojoParserCallback extends HTMLEditorKit.ParserCallback { /** * Holds the index of the current item in a numbered list. */ class Counter { int value; } /** * A flag whether the parser is currently in the body element. */ private boolean body; /** * A flag whether the parser is currently processing preformatted text, actually a counter to track nesting. */ private int preformatted; /** * The current indentation depth for the output. */ private int depth; /** * A stack of {@link Counter} objects corresponding to the nesting of (un-)ordered lists. A * <code>null</code> element denotes an unordered list. */ private Stack<Counter> numbering = new Stack<Counter>(); /** * A flag whether an implicit line break is pending in the output buffer. This flag is used to postpone the * output of implicit line breaks until we are sure that are not to be merged with other implicit line * breaks. */ private boolean pendingNewline; /** * A flag whether we have just parsed a simple tag. */ private boolean simpleTag; /** * The current buffer. */ private final StringBuilder sb; /** * @param sb not null */ public MojoParserCallback( StringBuilder sb ) { this.sb = sb; } /** {@inheritDoc} */ public void handleSimpleTag( HTML.Tag t, MutableAttributeSet a, int pos ) { simpleTag = true; if ( body && HTML.Tag.BR.equals( t ) ) { newline( false ); } } /** {@inheritDoc} */ public void handleStartTag( HTML.Tag t, MutableAttributeSet a, int pos ) { simpleTag = false; if ( body && ( t.breaksFlow() || t.isBlock() ) ) { newline( true ); } if ( HTML.Tag.OL.equals( t ) ) { numbering.push( new Counter() ); } else if ( HTML.Tag.UL.equals( t ) ) { numbering.push( null ); } else if ( HTML.Tag.LI.equals( t ) ) { Counter counter = numbering.peek(); if ( counter == null ) { text( "-\t" ); } else { text( ++counter.value + ".\t" ); } depth++; } else if ( HTML.Tag.DD.equals( t ) ) { depth++; } else if ( t.isPreformatted() ) { preformatted++; } else if ( HTML.Tag.BODY.equals( t ) ) { body = true; } } /** {@inheritDoc} */ public void handleEndTag( HTML.Tag t, int pos ) { if ( HTML.Tag.OL.equals( t ) || HTML.Tag.UL.equals( t ) ) { numbering.pop(); } else if ( HTML.Tag.LI.equals( t ) || HTML.Tag.DD.equals( t ) ) { depth--; } else if ( t.isPreformatted() ) { preformatted--; } else if ( HTML.Tag.BODY.equals( t ) ) { body = false; } if ( body && ( t.breaksFlow() || t.isBlock() ) && !HTML.Tag.LI.equals( t ) ) { if ( ( HTML.Tag.P.equals( t ) || HTML.Tag.PRE.equals( t ) || HTML.Tag.OL.equals( t ) || HTML.Tag.UL.equals( t ) || HTML.Tag.DL.equals( t ) ) && numbering.isEmpty() ) { pendingNewline = false; newline( pendingNewline ); } else { newline( true ); } } } /** {@inheritDoc} */ public void handleText( char[] data, int pos ) { /* * NOTE: Parsers before JRE 1.6 will parse XML-conform simple tags like <br/> as "<br>" followed by * the text event ">..." so we need to watch out for the closing angle bracket. */ int offset = 0; if ( simpleTag && data[0] == '>' ) { simpleTag = false; for ( ++offset; offset < data.length && data[offset] <= ' '; ) { offset++; } } if ( offset < data.length ) { String text = new String( data, offset, data.length - offset ); text( text ); } } /** {@inheritDoc} */ public void flush() { flushPendingNewline(); } /** * Writes a line break to the plain text output. * * @param implicit A flag whether this is an explicit or implicit line break. Explicit line breaks are * always written to the output whereas consecutive implicit line breaks are merged into a single * line break. */ private void newline( boolean implicit ) { if ( implicit ) { pendingNewline = true; } else { flushPendingNewline(); sb.append( '\n' ); } } /** * Flushes a pending newline (if any). */ private void flushPendingNewline() { if ( pendingNewline ) { pendingNewline = false; if ( sb.length() > 0 ) { sb.append( '\n' ); } } } /** * Writes the specified character data to the plain text output. If the last output was a line break, the * character data will automatically be prefixed with the current indent. * * @param data The character data, must not be <code>null</code>. */ private void text( String data ) { flushPendingNewline(); if ( sb.length() <= 0 || sb.charAt( sb.length() - 1 ) == '\n' ) { for ( int i = 0; i < depth; i++ ) { sb.append( '\t' ); } } String text; if ( preformatted > 0 ) { text = data; } else { text = data.replace( '\n', ' ' ); } sb.append( text ); } } /** * Find the best package name, based on the number of hits of actual Mojo classes. * * @param pluginDescriptor not null * @return the best name of the package for the generated mojo */ public static String discoverPackageName( PluginDescriptor pluginDescriptor ) { Map<String, Integer> packageNames = new HashMap<String, Integer>(); @SuppressWarnings( "unchecked" ) List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos(); if ( mojoDescriptors == null ) { return ""; } for ( MojoDescriptor descriptor : mojoDescriptors ) { String impl = descriptor.getImplementation(); if ( StringUtils.equals( descriptor.getGoal(), "help" ) && StringUtils.equals( "HelpMojo", impl ) ) { continue; } if ( impl.lastIndexOf( '.' ) != -1 ) { String name = impl.substring( 0, impl.lastIndexOf( '.' ) ); if ( packageNames.get( name ) != null ) { int next = ( packageNames.get( name ) ).intValue() + 1; packageNames.put( name, Integer.valueOf( next ) ); } else { packageNames.put( name, Integer.valueOf( 1 ) ); } } else { packageNames.put( "", Integer.valueOf( 1 ) ); } } String packageName = ""; int max = 0; for ( Map.Entry<String, Integer> entry : packageNames.entrySet() ) { int value = entry.getValue().intValue(); if ( value > max ) { max = value; packageName = entry.getKey(); } } return packageName; } /** * @param impl a Mojo implementation, not null * @param project a MavenProject instance, could be null * @return <code>true</code> is the Mojo implementation implements <code>MavenReport</code>, * <code>false</code> otherwise. * @throws IllegalArgumentException if any */ @SuppressWarnings( "unchecked" ) public static boolean isMavenReport( String impl, MavenProject project ) throws IllegalArgumentException { if ( impl == null ) { throw new IllegalArgumentException( "mojo implementation should be declared" ); } ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if ( project != null ) { List<String> classPathStrings; try { classPathStrings = project.getCompileClasspathElements(); if ( project.getExecutionProject() != null ) { classPathStrings.addAll( project.getExecutionProject().getCompileClasspathElements() ); } } catch ( DependencyResolutionRequiredException e ) { throw new IllegalArgumentException( e ); } List<URL> urls = new ArrayList<URL>( classPathStrings.size() ); for ( String classPathString : classPathStrings ) { try { urls.add( new File( classPathString ).toURL() ); } catch ( MalformedURLException e ) { throw new IllegalArgumentException( e ); } } classLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), classLoader ); } try { Class<?> clazz = Class.forName( impl, false, classLoader ); return MavenReport.class.isAssignableFrom( clazz ); } catch ( ClassNotFoundException e ) { return false; } } }